// TODO: CODE REVIEW & CLEANUP!
//
// JdsDriver.cs
//
// Implements HomeUX.Components.JdsDriver.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

/// <summary>
/// Implements a HomeUX driver for the JDS Technologies IR-XP2 "InfraRed Xpander", which transmits
/// IR (infrared remote control) commands.
/// </summary>
///
public class JdsDriver : Driver
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

    /// <summary>
    /// Maps the name of each IR command specified on the .config file to the corresponding
	/// <r>JdsCommand</r>.
    /// </summary>
    Dictionary<string, JdsCommand> _mapNameToCommand;

    /// <summary>
    /// The serial port specification string that defines the virtual or real serial port to open.
    /// </summary>
    string _serialPortSpec;

    /// <summary>
    /// Provides device-specific hardware access.
    /// </summary>
    JdsHardware _hardware;

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides & Related Methods
    //

    /// <summary>
    /// Overrides <r>Driver.OnInitialize</r>.
    /// </summary>
    ///
    protected override void OnInitialize()
    {
        // read attributes of the "<Device>" XML element
		_serialPortSpec = Configuration.GetRequiredAttribute("SerialPort");

        // read attributes of child XML element
        _mapNameToCommand = new Dictionary<string, JdsCommand>(JdsHardware.MaxAddress);
		foreach (ConfigurationElement element in Configuration.Children)
		{
            switch (element.Name)
			{

			case "Command":

				var command = new JdsCommand(element.GetRequiredAttribute("Name"),
                    element.GetRequiredIntAttribute("Address", 1, JdsHardware.MaxAddress),
                    element.GetRequiredIntAttribute("PlayCount", 1, JdsHardware.MaxPlayCount),
                    ParseEmitterPorts(element.GetRequiredAttribute("EmitterPorts"), element));
				try
				{
					_mapNameToCommand.Add(command.Name, command);
				}
				catch (ArgumentException)
				{
					throw new ConfigurationException(element,
						"Duplicate command name: {0}", command.Name);
				}
				break;

			}
		}
    }

    /// <summary>
    /// Overrides <r>Driver.OnEnable</r>.
    /// </summary>
    ///
    protected override void OnEnable()
    {
        QueueRequest(new JdsEnableRequest() { Driver = this }, false);
    }

    /// <summary>
    /// Overrides <r>Driver.OnDisable</r>.
    /// </summary>
    ///
    protected override void OnDisable()
    {
		CloseHardware();
        DisableCompleted(); // disable completed synchronously
    }

    /// <summary>
    /// Overrides <r>Driver.SerializeStateForClient</r>.
    /// </summary>
    ///
    /// <param name="sessionId">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    /// <param name="writer">See <r>Driver.SerializeStateForClient</r>.</param>
    ///
    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        SyncPath commandsPath = SyncPath.Build(Name, "Commands");
        foreach (JdsCommand command in _mapNameToCommand.Values)
            SerializeSyncNodeUpdate(writer, false, commandsPath, command.Name);
    }

    /// <summary>
    /// See <r>Driver.ProcessSyncNodeCommand</r>.
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeCommand</r>.</param>
    ///
    /// <param name="command">See <r>Driver.ProcessSyncNodeCommand</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeCommand</r>.</param>
    ///
    public override void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command,
        MessageBuilder replyBuilder)
    {
        switch (command.Path.MatchRequiredPart(1, "Commands"))
        {

        case 0: // "Commands"

            ProcessSyncNodeCommand_Commands(originator, command, replyBuilder);
            break;

        }
    }

    /// <summary>
    /// Called when a client executes a command on a SyncTree node whose path is of the form
    /// "device-name\Commands\...".
    /// </summary>
    ///
    /// <param name="originator">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="syncNodeCommand">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    /// <param name="replyBuilder">See <r>Driver.ProcessSyncNodeUpdate</r>.</param>
    ///
    void ProcessSyncNodeCommand_Commands(Guid originator, SyncNodeCommand syncNodeCommand,
        MessageBuilder replyBuilder)
    {
        // <update.Path> is "<device-name>\Commands\<command-name>"
        // example: "JDS\Commands\TiVo TiVo"

		// set <command> to the JdsCommand corresponding to <syncNodeCommand>
        string commandName = syncNodeCommand.Path.GetRequiredLastPart(2);
        JdsCommand command;
        if (!_mapNameToCommand.TryGetValue(commandName, out command))
            throw new SyncException(syncNodeCommand, "Invalid command name \"{0}\"", commandName);

		// queue <command> for execution
        if (syncNodeCommand.Verb == "Play")
        {
            QueueRequest(new JdsPlayRequest()
            {
                Driver = this,
                Operation = syncNodeCommand,
                Originators = new List<Guid>() { originator },
                Command = command
            }, true);
        }
        else
        {
            throw new SyncException(syncNodeCommand,
				"Invalid verb \"{0}\"; the only valid verb is \"Play\"",
                syncNodeCommand.Verb);
        }
    }

    /// <summary>
    /// Overrides <r>Driver.MergeRequests</r>.
    /// </summary>
    ///
    /// <param name="futureRequests">See <r>Driver.MergeRequests</r>.</param>
    ///
    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
        // strategy: merge the first request with each subsequent request for which these criteria
		// are met:
        //   -- both first and subsequent requests are of type JdsPlayRequest;
        //   -- both specify the same IR sample address and emitter ports
		while (true)
		{
			// we need at least 2 requests in order to do a merge
			if (futureRequests.Count < 2)
				return;

			// set <first> to the first request; return if it's not a JdsPlayRequest request; note
			// that we'll repeat this (since we're in a loop)
			var first = futureRequests[0] as JdsPlayRequest;
			if (first == null)
				return;

			// set <next> to the second request; return if it's not a JdsPlayRequest request
            var next = futureRequests[1] as JdsPlayRequest;
            if (next == null)
                return;

			// return if the two commands don't specify the same IR sample address and emitter
			// ports -- i.e. if they're the same except maybe for play count
            if ((first.Command.Address != next.Command.Address) ||
                (first.Command.EmitterPorts != next.Command.EmitterPorts))
                return;

			// merge the two commands into the first one
            first.Originators.AddRange(next.Originators);
            first.Command = new JdsCommand(first.Command.Name, first.Command.Address, 
                first.Command.PlayCount + next.Command.PlayCount, first.Command.EmitterPorts);
            futureRequests.RemoveAt(1);
            Trace("Merged requests");
        }
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    /// <summary>
    /// Implements <n>IDisposable.Dispose</n>.
    /// </summary>
    ///
    public override void Dispose()
    {
        if (_hardware != null)
        {
            _hardware.Dispose();
            _hardware = null;
        }
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Other Methods
	//

    /// <summary>
    /// Assuming <r>_hardware</r> is null, this method creates an <r>JdsHardware</r> object, used
    /// to access JDS hardware, and sets <r>_hardware</r> to refer to it.
    /// </summary>
	///
    void OpenHardware()
    {
        VirtualSerialPort serialPort = null;
        try
        {
            // set <serialPort> to a new serial port, as specified by <_serialPortSpec>
            serialPort = VirtualSerialPort.GetPort(_serialPortSpec);

            // set <_hardware> to a newly-allocated JdsHardware object that will use <serialPort>
            // to communicate with the hardware
            _hardware = new JdsHardware(WorkQueue, serialPort, Name);

            // hook up <_hardware> events
            // IMPORTANT: do corresponding "-=" in CloseHardware
            _hardware.CommandTimedOut += OnHardwareCommandTimedOut;
            _hardware.WarningsReported += OnHardwareWarningsReported;
            // IMPORTANT: do corresponding "-=" in CloseHardware

            // open <_hardware>; this will fail if the serial port cannot be opened
            _hardware.Open();
        }
        finally
        {
            // if <serialPort> was allocated but ownership of it wasn't transferred to <_hardware>,
			// free <serialPort> to prevent a memory leak
            if ((serialPort != null) && (_hardware == null))
            {
                serialPort.Dispose();
                serialPort = null;
            }
        }
    }

    /// <summary>
    /// Assuming <r>_hardware</r> is not null, this method frees it.
    /// </summary>
    ///
	void CloseHardware()
	{
        // disconnect <_hardware> events
		_hardware.CommandTimedOut -= OnHardwareCommandTimedOut;
		_hardware.WarningsReported -= OnHardwareWarningsReported;

		// dispose of <_hardware>
        _hardware.Dispose();
        _hardware = null;
	}

    /// <summary>
    /// See <r>SimpleSerialHardware{}.CommandTimedOut</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
    /// <param name="e">See <r>SimpleSerialHardware{}.CommandTimedOut</r>.</param>
    ///
	public void OnHardwareCommandTimedOut(object sender,
		SimpleSerialHardwareCommandTimedOutEventArgs e)
	{
        ReportAlert(MessageAlertType.Warning, "\"{0}\" command timed out", e.Command);
	}

    /// <summary>
    /// See <r>JdsHardware.WarningsReported</r>.
    /// </summary>
    ///
    /// <param name="sender">See <r>JdsHardware.WarningsReported</r>.</param>
    ///
    /// <param name="e">See <r>JdsHardware.WarningsReported</r>.</param>
    ///
	public void OnHardwareWarningsReported(object sender, JdsHardwareWarningsReportedEventArgs e)
	{
		foreach (string message in e.Messages)
            ReportAlert(MessageAlertType.Warning, message);
	}

    /// <summary>
    /// Parses a string containing a list of IR emitter port number.
    /// </summary>
    ///
    /// <param name="stringValue">The string to parse; e.g. "1,2,3,4".</param>
    ///
    /// <param name="element">The XML element containing the string; used for error reporting
	/// 	purposes.</param>
	///
	/// <exception cref="ConfigurationException">
	/// The string is not in the correct format.
	/// </exception>
	///
    int ParseEmitterPorts(string stringValue, ConfigurationElement element)
    {
		int emitterPorts = 0;
		try
		{
			foreach (string part in stringValue.Split(','))
			{
				int emitterPort = XmlConvert.ToInt32(part);
				if ((emitterPort < 1) || (emitterPort > JdsHardware.EmitterPortCount))
					throw new FormatException();
				emitterPorts = emitterPorts | (1 << (emitterPort - 1));
			}
		}
		catch (Exception ex)
		{
			if ((ex is FormatException) || (ex is OverflowException))
			{
				throw new ConfigurationException(element,
					"Expected a comma-separated list of IR emitter ports, each in the range 1 to {0} (inclusive); got: {1}",
					JdsHardware.EmitterPortCount, stringValue);
			}
		}
		return emitterPorts;
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // JdsRequest and Derived Types
    //

    /// <summary>
    /// A request from a client.
    /// </summary>
    ///
	abstract class JdsRequest : Driver.Request
	{
        protected new JdsDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (JdsDriver)base.Driver;
            }
        }
	}

    /// <summary>
    /// A requst to enable the device.
    /// </summary>
    ///
    [DebuggerDisplay("Enable driver")]
    class JdsEnableRequest : JdsRequest, Driver.IEnableRequest
    {
        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            // open the hardware
			try
			{
                Driver.OpenHardware();
            }
            catch (VirtualSerialPortException ex)
            {
                Error = Util.FormatExceptionMessage(ex);
                yield break; // end JdsRequest
            }

            // reset the hardware
            var reset = new ResultHolder<GenericReply>(this);
            Driver._hardware.Reset(reset.OnDone);
            yield return true; // wait for async reply
            if (reset.Result == null)
            {
                Error = "Reset failed";
                yield break; // end JdsRequest
            }
        }
    }

    /// <summary>
    /// A requst to play an IR sample.
    /// </summary>
    ///
    class JdsPlayRequest : JdsRequest
    {
        /// <summary>
        /// The JDS command specifying the IR sample to play.
        /// </summary>
        public JdsCommand Command;

        /// <summary>
        /// Overrides <r>StateMachine.Implementation</r>.
        /// </summary>
        ///
        protected override IEnumerable<bool> Implementation()
        {
            var playIrSample = new ResultHolder<GenericReply>(this);
            Driver.Trace(true, "HW: Playing IR sample {0} ({1})", Command.Address, Command.Name);
            Driver._hardware.PlayIrSample(Command.Address, Command.PlayCount,
                Command.EmitterPorts, playIrSample.OnDone);
            yield return true; // wait for async reply
            if (playIrSample.Result == null)
            {
                Error = "PlayIrSample failed";
                yield break; // end JdsRequest
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Other Nested Types
    //

    /// <summary>
    /// Represents an IR command that specifies a command name (from the .config file), the
	/// address of the IR sample to play, and playback parameters.
    /// </summary>
    ///
    [DebuggerDisplay("{DebuggerDisplay}")]
    class JdsCommand
    {
        /// <summary>
        /// The name of this command.
        /// </summary>
        public string Name { get; private set; }

        /// <summary>
        /// The 1-based address of the IR sample to play.  The JDS device must be preprogrammed
		/// with the IR sample.
        /// </summary>
        public int Address { get; private set; }

        /// <summary>
        /// The number of times to play the IR sample when this command is executed.
        /// </summary>
        public int PlayCount { get; private set; }

        /// <summary>
        /// Which IR emitter ports to transmit from.  Bit <i>i</i> of this value is 1 if port
		/// <i>i + 1</i> should be used, for <i>i</i> in the range 0 to
        /// <r>JdsHardware.EmitterPortCount</r> <i>- 1</i>, inclusive.
        /// </summary>
        public int EmitterPorts { get; private set; }

        /// <summary>
		/// Initializes an instance of this class.
        /// </summary>
		///
        /// <param name="name">The name of this command.</param>
		///
        /// <param name="address">The 1-based address of the IR sample to play.</param>
		///
        /// <param name="playCount">
        /// The number of times to play the IR sample when this command is executed.
        /// </param>
		///
        /// <param name="emitterPorts">Which IR emitter ports to transmit from.  Bit <i>i</i> of
		/// 	this value is 1 if port <i>i + 1</i> should be used, for <i>i</i> in the range 0 to
        /// 	<r>JdsHardware.EmitterPortCount</r> <i>- 1</i>, inclusive.</param>
		///
		public JdsCommand(string name, int address, int playCount, int emitterPorts)
		{
			Name = name;
			Address = address;
			PlayCount = playCount;
			EmitterPorts = emitterPorts;
		}

        /// <summary>
        /// Returns a value used by <c>[DebuggerDisplay]</c>, encapsulated to prevent the debugger
        /// from displaying it in quotes.
        /// </summary>
        public object DebuggerDisplay
        {
            get
            {
                return new EncapsulatedString("Command \"{0}\" (#{1}) - PlayCount={2}",
					Name, Address, PlayCount);
            }
        }
    }
}

}

